package de.programmerspain.rv3sf.examples;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.programmerspain.rv3sf.api.GammaTable;
import de.programmerspain.rv3sf.api.RainbowduinoV3;

/**
 * An abstract base class used by all example classes. This class takes care
 * of creating and storing the <code>RainbowduinoV3</code> instances based
 * on the number of given serial device names via the command line arguments.
 * 
 * It also creates a scheduledExecutorService to update all RainbowduinoV3
 * instances in the given interval and a second executorService which takes
 * care of updating all RainbowduinoV3 simultaneously in a dedicated thread.
 * 
 * @author Markus Lang (m@rkus-lang.de) | http://programmers-pain.de/ | https://code.google.com/p/rainbowduino-v3-streaming-firmware/
 */
public abstract class AbstractExample {
	private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExample.class);
	
	private RainbowduinoV3[] rainbowduinoV3s;
	
	private ScheduledExecutorService scheduledExecutorService;
	private ExecutorService executorService;
	private CountDownLatch drawStartGate;
	private CountDownLatch drawEndGate;
	
	protected int[] frame;
	
	public AbstractExample(String args[]) {
		// query command line arguments for the serial port device name
		if (args.length == 0) {
			LOGGER.error("You need to specify at least one serial port device name!");
			LOGGER.error("Usage: 'java <class name> <serial port name> (e.G. '/dev/ttyUSB0')'");
			System.exit(1);
		}
		
		// initialize internal arrays
		this.rainbowduinoV3s = new RainbowduinoV3[args.length];
		this.frame = new int[64];

		// construct gamma table in bypass mode
		GammaTable gammaTable = new GammaTable();
		
		// create a dedicated rainbowduinoV3 instance
		// for the given serial device names
		for (int i = 0 ; i < args.length; i++) {
			this.rainbowduinoV3s[i] = new RainbowduinoV3(args[i], gammaTable);
		}
		
		// create a scheduledExecutorService used to trigger an frame update 
		this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
		// create a executorService used to update all rainbowduinoV3 instances simultaneously
		this.executorService = Executors.newCachedThreadPool();
		
		// register VM shutdown hook so that we close the
		// rainbowduinoV3 instances while stopping the Java VM
		Runtime.getRuntime().addShutdownHook(new Thread(
				new Runnable() {
					@Override
					public void run() {
						for (RainbowduinoV3 rainbowduinoV3 : rainbowduinoV3s) {
							rainbowduinoV3.close();
						}
						executorService.shutdown();
						scheduledExecutorService.shutdown();
					}
				}, 
				"VmShutdownThread"
		));
	}
	
	protected void start(int period, TimeUnit timeUnit) {
		// schedule an internal runnable with the given interval
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				// wait for rainbowduinoV3 instances to finish their sendFrame() methods from the previous call of this method
				if (drawEndGate != null) {
					try {
						drawEndGate.await();
					} catch (InterruptedException e) {
						LOGGER.error("wating for all rainbowduinoV3 instances to finish their sendFrame() method got interrupted: " + e.getMessage(), e);
					}
				}
				
				// create countDownLatch instances used to call all sendFrame() methods simultaneously
				// and to block until all calls have been finished via the end gate instance
				drawStartGate = new CountDownLatch(1);
				drawEndGate = new CountDownLatch(rainbowduinoV3s.length);
				
				// request an frame update from the implementing class
				updateFrame();
				
				// create a runnable instance for each rainbowduinoV3 instance
				// that will be used to call the sendFrame() method
				for (final RainbowduinoV3 rainbowduinoV3 : rainbowduinoV3s) {
					Runnable sendFrameRunnable = new Runnable() {
						@Override
						public void run() {
							try {
								drawStartGate.await();
								try {
									rainbowduinoV3.sendFrame(frame);
								} finally {
									drawEndGate.countDown();
								}
							} catch (InterruptedException e) {
								LOGGER.error("waiting for the drawStartGate got interrupted!", e);
							}
						}
					};
					// pass runnable to the executorService
					executorService.execute(sendFrameRunnable);
				}
				
				// trigger simultaneous update of all rainbowduinoV3 instances
				drawStartGate.countDown();
			}
		};
		
		// schedule the sendFrame runnable at the given internal
		this.scheduledExecutorService.scheduleAtFixedRate(runnable, 0, period, timeUnit);
	}
	
	// abstract method that will be called to update the internal frame array
	protected abstract void updateFrame();
}
